Skip to content

Ensure single trailing newline in generated files - #6781

Merged
SteffenDE merged 3 commits into
phoenixframework:mainfrom
praialabs:fix-template-whitespace
Aug 3, 2026
Merged

Ensure single trailing newline in generated files#6781
SteffenDE merged 3 commits into
phoenixframework:mainfrom
praialabs:fix-template-whitespace

Conversation

@rhcarvalho

Copy link
Copy Markdown
Contributor

And no unintentional consecutive blank lines in the generated content.

Those two issues are rather aesthetic, but downstream users notice and they are a common source of code churn and maintenance overheard that we can prevent from now on.

The single trailing newline is a common convention in Unix and POSIX systems, and it is also what the Elixir formatter does (Code.format_file! always appends a trailing newline 1).

The consecutive blank lines in Elixir code are also automatically removed by the Elixir formatter. It is not a strict rule for other files, but generally they are not expected and most of the time added unintentionally, e.g. when using conditional EEx templates or concatenating strings (AGENTS.md / usage rules).

Except for a handful of generated files (favicon.ico, phoenix.png and *.pem certificates), all generated files are candidates for those two rules. Instead of updating lots of tests (which would cause a lot of churn and be a future maintenance burden, easy to miss in new tests), we update MixHelper.assert_file/1 to check for those two rules in all current and future generated files. If we need more exceptions in the future, it is easy to change assert_file/1 to add them.

@rhcarvalho

Copy link
Copy Markdown
Contributor Author

Integration test failure in was https://github.com/phoenixframework/phoenix/actions/runs/30761860392/job/91533719530?pr=6781 ** (Exqlite.Error) Database busy

Comment on lines +11 to +12
<%= if (fields = Mix.Phoenix.Schema.format_fields_for_schema(schema)) != "" do %><%= fields %>
<% end %><%= for {_, k, _, _} <- schema.assocs do %> field <%= inspect k %>, <%= if schema.binary_id do %>:binary_id<% else %>:id<% end %>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should revisit #6015, then it wouldn't matter as much how the templates do whitespace

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see how that is related, but it wouldn't solve all cases that we cover here in this PR. Among other things mix format would not touch:

  • AGENTS.md, or any other markdown content
  • *.pot Gettext files
  • app.js
  • .gitignore
  • default.css

On the other hand, mix format is still complementary for the "final touch", and would handle things we don't handle here, for example reformatting lines that may become too long depending on a module name.


This one particular code hunk is perhaps the trickiest change because it handles the case where there are no fields in the schema. Without the if we'd unconditionally render a newline, such that:

  1. Schema generated without attributes (mix phx.gen.schema Blog.Post posts):

      schema "posts" do
    
    
        timestamps(type: :naive_datetime)
      end
  2. Schema generated with only reference attributes (mix phx.gen.schema Blog.Post posts user_id:references:users):

      schema "posts" do
    
        field :user_id, :id
    
        timestamps()
      end

Both cases would be handled by a pass of mix format.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering the precedent of format_fields_for_schema/1, we can simplify this particular template with another helper format_schema_body/2 that takes care of joining the multiple components without this line break dance in the template.

I can volunteer to revisit #6015 as a follow up, essentially applying Code.format_string!/2 to generated Elixir code, which would handle the remaining cases like long lines.

rhcarvalho added a commit to praialabs/elixir that referenced this pull request Aug 2, 2026
Replicate the approach from phoenixframework/phoenix#6781, since clearly
the Phoenix installer and `mix new` share the `assert_file/1` and
`assert_file/2` helpers.

All generated files are candidates for these two rules:
1. Every file must end with a single trailing newline.
2. No file may contain consecutive blank lines.

Instead of updating lots of tests (which would cause a lot of churn and
be a future maintenance burden, easy to miss in new tests), update the
helpers to enforce these rules on every file. Exceptions can be easily
added in the future if needed.
@rhcarvalho
rhcarvalho force-pushed the fix-template-whitespace branch from 1991fcf to b679931 Compare August 3, 2026 07:12
@rhcarvalho

Copy link
Copy Markdown
Contributor Author

Rebased onto latest main to pick up flaky integration test fix from #6783.

// If you have dependencies that try to import CSS, esbuild will generate a separate `app.css` file.
// To load it, simply add a second `<link>` to your `root.html.heex` file.
<%= if @html do %>
// To load it, simply add a second `<link>` to your `root.html.heex` file.<%= if @html do %>

@SteffenDE SteffenDE Aug 3, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// To load it, simply add a second `<link>` to your `root.html.heex` file.<%= if @html do %>
// To load it, simply add a second `<link>` to your `root.html.heex` file.<%= if @html do %>

I think this one is deliberate

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, good catch. The intention was to remove additional newlines at the end of file when html: false. The correct change that preserves the blank line separating the comment blocks is:

Suggested change
// To load it, simply add a second `<link>` to your `root.html.heex` file.<%= if @html do %>
// To load it, simply add a second `<link>` to your `root.html.heex` file.<%= if @html do %>

I.e., the if moves up, but the blank line remains. Verified across all four permutations of :html and :live.

Comment on lines +9 to +10
## Channels<%= if existing_channel do %>

channel "<%= existing_channel[:singular] %>:*", <%= existing_channel[:module] %>Channel
<% else %>
channel "<%= existing_channel[:singular] %>:*", <%= existing_channel[:module] %>Channel<% else %>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would render as

## Channels
channel ...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. This is one weird case.

The generated code is an Elixir module-level comment before the channel macro call.

  1. It seems non-standard that it has a double hash-sign.
  2. We don't do such sectional comments in Endpoint sockets or plugs.

I'm pro removing the comment altogether. Will do in a separate commit so we can evaluate and decide whether to keep or drop.

@rhcarvalho
rhcarvalho marked this pull request as draft August 3, 2026 11:21
@rhcarvalho

Copy link
Copy Markdown
Contributor Author

I'm doing an additional and more thorough review of each permutation of the template changes to avoid introducing unintended behavior/whitespace changes.

@rhcarvalho
rhcarvalho force-pushed the fix-template-whitespace branch from b679931 to 6a27152 Compare August 3, 2026 11:29
And no unintentional consecutive blank lines in the generated content.

Those two issues are rather aesthetic, but downstream users notice and
they are a common source of code churn and maintenance overheard that we
can prevent from now on.

The single trailing newline is a common convention in Unix and POSIX
systems, and it is also what the Elixir formatter does
(`Code.format_file!` always appends a trailing newline [1]).

The consecutive blank lines in Elixir code are also automatically
removed by the Elixir formatter. It is not a strict rule for other
files, but generally they are not expected and most of the time added
unintentionally, e.g. when using conditional EEx templates or
concatenating strings (AGENTS.md / usage rules).

Except for a handful of generated files (favicon.ico, phoenix.png and
*.pem certificates), all generated files are candidates for those two
rules. Instead of updating lots of tests (which would cause a lot of
churn and be a future maintenance burden, easy to miss in new tests), we
update `MixHelper.assert_file/1` to check for those two rules in all
current and future generated files. If we need more exceptions in the
future, it is easy to change `assert_file/1` to add them.

[1]: https://github.com/elixir-lang/elixir/blob/545dddf138e4cb1ee874e6f2c26882c9b438f551/lib/elixir/lib/code.ex#L1137-L1141
@rhcarvalho
rhcarvalho force-pushed the fix-template-whitespace branch from 6a27152 to f213110 Compare August 3, 2026 12:18
Instead of fighting with whitespace in the template, follow the
precedent of `format_fields_for_schema/1` and create a helper that
formats the entire schema body, including fields, associations, scope,
and timestamps.

All permutations of the presence of fields, associations, and scope are
tested to ensure correct formatting.
We don't do this in other files, and the double hash comment is also
non-standard.

@rhcarvalho rhcarvalho left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Detailed human review of each change considering every permutation of the templates before and after, showing the exact whitespace fixes (in per file comments). Always one of:

  1. Missing newline at EOF
  2. Extra blank line at EOF
  3. Two or more consecutive blank lines in the middle of a generated file

The changes to the assert_file test helper guarantee we won't reintroduce those classes of whitespace issues in the future, hopefully reducing future churn.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Computed Outputs Across Conditional Permutations

Permutation: html: true, live: true (Output changed by commit)

Unified Output Diff
--- Before
+++ After
@@ -80,4 +80,3 @@
     window.liveReloader = reloader
   })
 }
-
Full Rendered Outputs (Before & After)
Before Commit
// If you want to use Phoenix channels, run `mix help phx.gen.channel`
// to get started and then uncomment the line below.
// import "./user_socket.js"

// You can include dependencies in two ways.
//
// The simplest option is to put them in assets/vendor and
// import them using relative paths:
//
//     import "../vendor/some-package.js"
//
// Alternatively, you can `npm install some-package --prefix assets` and import
// them using a path starting with the package name:
//
//     import "some-package"
//
// If you have dependencies that try to import CSS, esbuild will generate a separate `app.css` file.
// To load it, simply add a second `<link>` to your `root.html.heex` file.

// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
// Establish Phoenix Socket and LiveView configuration.
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import {hooks as colocatedHooks} from "phoenix-colocated/my_app_web"
import topbar from "../vendor/topbar"

const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
const liveSocket = new LiveSocket("/live", Socket, {
  longPollFallbackMs: 2500,
  params: {_csrf_token: csrfToken},
  hooks: {...colocatedHooks},
})

// Show progress bar on live navigation and form submits
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
window.addEventListener("phx:page-loading-stop", _info => topbar.hide())

// connect if there are any LiveViews on the page
liveSocket.connect()

// expose liveSocket on window for web console debug logs and latency simulation:
// >> liveSocket.enableDebug()
// >> liveSocket.enableLatencySim(1000)  // enabled for duration of browser session
// >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket

// The lines below enable quality of life phoenix_live_reload
// development features:
//
//     1. stream server logs to the browser console
//     2. click on elements to jump to their definitions in your code editor
//
if (process.env.NODE_ENV === "development") {
  window.addEventListener("phx:live_reload:attached", ({detail: reloader}) => {
    // Enable server log streaming to client.
    // Disable with reloader.disableServerLogs()
    reloader.enableServerLogs()

    // Open configured PLUG_EDITOR at file:line of the clicked element's HEEx component
    //
    //   * click with "c" key pressed to open at caller location
    //   * click with "d" key pressed to open at function component definition location
    let keyDown
    window.addEventListener("keydown", e => keyDown = e.key)
    window.addEventListener("keyup", _e => keyDown = null)
    window.addEventListener("click", e => {
      if(keyDown === "c"){
        e.preventDefault()
        e.stopImmediatePropagation()
        reloader.openEditorAtCaller(e.target)
      } else if(keyDown === "d"){
        e.preventDefault()
        e.stopImmediatePropagation()
        reloader.openEditorAtDef(e.target)
      }
    }, true)

    window.liveReloader = reloader
  })
}
After Commit
// If you want to use Phoenix channels, run `mix help phx.gen.channel`
// to get started and then uncomment the line below.
// import "./user_socket.js"

// You can include dependencies in two ways.
//
// The simplest option is to put them in assets/vendor and
// import them using relative paths:
//
//     import "../vendor/some-package.js"
//
// Alternatively, you can `npm install some-package --prefix assets` and import
// them using a path starting with the package name:
//
//     import "some-package"
//
// If you have dependencies that try to import CSS, esbuild will generate a separate `app.css` file.
// To load it, simply add a second `<link>` to your `root.html.heex` file.

// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
// Establish Phoenix Socket and LiveView configuration.
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import {hooks as colocatedHooks} from "phoenix-colocated/my_app_web"
import topbar from "../vendor/topbar"

const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
const liveSocket = new LiveSocket("/live", Socket, {
  longPollFallbackMs: 2500,
  params: {_csrf_token: csrfToken},
  hooks: {...colocatedHooks},
})

// Show progress bar on live navigation and form submits
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
window.addEventListener("phx:page-loading-stop", _info => topbar.hide())

// connect if there are any LiveViews on the page
liveSocket.connect()

// expose liveSocket on window for web console debug logs and latency simulation:
// >> liveSocket.enableDebug()
// >> liveSocket.enableLatencySim(1000)  // enabled for duration of browser session
// >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket

// The lines below enable quality of life phoenix_live_reload
// development features:
//
//     1. stream server logs to the browser console
//     2. click on elements to jump to their definitions in your code editor
//
if (process.env.NODE_ENV === "development") {
  window.addEventListener("phx:live_reload:attached", ({detail: reloader}) => {
    // Enable server log streaming to client.
    // Disable with reloader.disableServerLogs()
    reloader.enableServerLogs()

    // Open configured PLUG_EDITOR at file:line of the clicked element's HEEx component
    //
    //   * click with "c" key pressed to open at caller location
    //   * click with "d" key pressed to open at function component definition location
    let keyDown
    window.addEventListener("keydown", e => keyDown = e.key)
    window.addEventListener("keyup", _e => keyDown = null)
    window.addEventListener("click", e => {
      if(keyDown === "c"){
        e.preventDefault()
        e.stopImmediatePropagation()
        reloader.openEditorAtCaller(e.target)
      } else if(keyDown === "d"){
        e.preventDefault()
        e.stopImmediatePropagation()
        reloader.openEditorAtDef(e.target)
      }
    }, true)

    window.liveReloader = reloader
  })
}

Permutation: html: true, live: false (Output changed by commit)

Unified Output Diff
--- Before
+++ After
@@ -81,10 +81,9 @@
 //   })
 // }
 
-
 // Handle flash close
 document.querySelectorAll("[role=alert][data-flash]").forEach((el) => {
   el.addEventListener("click", () => {
     el.setAttribute("hidden", "")
   })
-})
\ No newline at end of file
+})
Full Rendered Outputs (Before & After)
Before Commit
// If you want to use Phoenix channels, run `mix help phx.gen.channel`
// to get started and then uncomment the line below.
// import "./user_socket.js"

// You can include dependencies in two ways.
//
// The simplest option is to put them in assets/vendor and
// import them using relative paths:
//
//     import "../vendor/some-package.js"
//
// Alternatively, you can `npm install some-package --prefix assets` and import
// them using a path starting with the package name:
//
//     import "some-package"
//
// If you have dependencies that try to import CSS, esbuild will generate a separate `app.css` file.
// To load it, simply add a second `<link>` to your `root.html.heex` file.

// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
// Establish Phoenix Socket and LiveView configuration.
// import {Socket} from "phoenix"
// import {LiveSocket} from "phoenix_live_view"
// import {hooks as colocatedHooks} from "phoenix-colocated/my_app_web"
// import topbar from "../vendor/topbar"

// const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
// const liveSocket = new LiveSocket("/live", Socket, {
//   longPollFallbackMs: 2500,
//   params: {_csrf_token: csrfToken},
//   hooks: {...colocatedHooks},
// })

// Show progress bar on live navigation and form submits
// topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
// window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
// window.addEventListener("phx:page-loading-stop", _info => topbar.hide())

// connect if there are any LiveViews on the page
// liveSocket.connect()

// expose liveSocket on window for web console debug logs and latency simulation:
// >> liveSocket.enableDebug()
// >> liveSocket.enableLatencySim(1000)  // enabled for duration of browser session
// >> liveSocket.disableLatencySim()
// window.liveSocket = liveSocket

// The lines below enable quality of life phoenix_live_reload
// development features:
//
//     1. stream server logs to the browser console
//     2. click on elements to jump to their definitions in your code editor
//
// if (process.env.NODE_ENV === "development") {
//   window.addEventListener("phx:live_reload:attached", ({detail: reloader}) => {
//     // Enable server log streaming to client.
//     // Disable with reloader.disableServerLogs()
//     reloader.enableServerLogs()
// 
//     // Open configured PLUG_EDITOR at file:line of the clicked element's HEEx component
//     //
//     //   * click with "c" key pressed to open at caller location
//     //   * click with "d" key pressed to open at function component definition location
//     let keyDown
//     window.addEventListener("keydown", e => keyDown = e.key)
//     window.addEventListener("keyup", _e => keyDown = null)
//     window.addEventListener("click", e => {
//       if(keyDown === "c"){
//         e.preventDefault()
//         e.stopImmediatePropagation()
//         reloader.openEditorAtCaller(e.target)
//       } else if(keyDown === "d"){
//         e.preventDefault()
//         e.stopImmediatePropagation()
//         reloader.openEditorAtDef(e.target)
//       }
//     }, true)
// 
//     window.liveReloader = reloader
//   })
// }


// Handle flash close
document.querySelectorAll("[role=alert][data-flash]").forEach((el) => {
  el.addEventListener("click", () => {
    el.setAttribute("hidden", "")
  })
})```

##### After Commit
```javascript
// If you want to use Phoenix channels, run `mix help phx.gen.channel`
// to get started and then uncomment the line below.
// import "./user_socket.js"

// You can include dependencies in two ways.
//
// The simplest option is to put them in assets/vendor and
// import them using relative paths:
//
//     import "../vendor/some-package.js"
//
// Alternatively, you can `npm install some-package --prefix assets` and import
// them using a path starting with the package name:
//
//     import "some-package"
//
// If you have dependencies that try to import CSS, esbuild will generate a separate `app.css` file.
// To load it, simply add a second `<link>` to your `root.html.heex` file.

// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
// Establish Phoenix Socket and LiveView configuration.
// import {Socket} from "phoenix"
// import {LiveSocket} from "phoenix_live_view"
// import {hooks as colocatedHooks} from "phoenix-colocated/my_app_web"
// import topbar from "../vendor/topbar"

// const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
// const liveSocket = new LiveSocket("/live", Socket, {
//   longPollFallbackMs: 2500,
//   params: {_csrf_token: csrfToken},
//   hooks: {...colocatedHooks},
// })

// Show progress bar on live navigation and form submits
// topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
// window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
// window.addEventListener("phx:page-loading-stop", _info => topbar.hide())

// connect if there are any LiveViews on the page
// liveSocket.connect()

// expose liveSocket on window for web console debug logs and latency simulation:
// >> liveSocket.enableDebug()
// >> liveSocket.enableLatencySim(1000)  // enabled for duration of browser session
// >> liveSocket.disableLatencySim()
// window.liveSocket = liveSocket

// The lines below enable quality of life phoenix_live_reload
// development features:
//
//     1. stream server logs to the browser console
//     2. click on elements to jump to their definitions in your code editor
//
// if (process.env.NODE_ENV === "development") {
//   window.addEventListener("phx:live_reload:attached", ({detail: reloader}) => {
//     // Enable server log streaming to client.
//     // Disable with reloader.disableServerLogs()
//     reloader.enableServerLogs()
// 
//     // Open configured PLUG_EDITOR at file:line of the clicked element's HEEx component
//     //
//     //   * click with "c" key pressed to open at caller location
//     //   * click with "d" key pressed to open at function component definition location
//     let keyDown
//     window.addEventListener("keydown", e => keyDown = e.key)
//     window.addEventListener("keyup", _e => keyDown = null)
//     window.addEventListener("click", e => {
//       if(keyDown === "c"){
//         e.preventDefault()
//         e.stopImmediatePropagation()
//         reloader.openEditorAtCaller(e.target)
//       } else if(keyDown === "d"){
//         e.preventDefault()
//         e.stopImmediatePropagation()
//         reloader.openEditorAtDef(e.target)
//       }
//     }, true)
// 
//     window.liveReloader = reloader
//   })
// }

// Handle flash close
document.querySelectorAll("[role=alert][data-flash]").forEach((el) => {
  el.addEventListener("click", () => {
    el.setAttribute("hidden", "")
  })
})

Permutation: html: false, live: true (Output unchanged)

Unified Output Diff
(No output changes)
Full Rendered Outputs (Before & After)
Before Commit
// If you want to use Phoenix channels, run `mix help phx.gen.channel`
// to get started and then uncomment the line below.
// import "./user_socket.js"

// You can include dependencies in two ways.
//
// The simplest option is to put them in assets/vendor and
// import them using relative paths:
//
//     import "../vendor/some-package.js"
//
// Alternatively, you can `npm install some-package --prefix assets` and import
// them using a path starting with the package name:
//
//     import "some-package"
//
// If you have dependencies that try to import CSS, esbuild will generate a separate `app.css` file.
// To load it, simply add a second `<link>` to your `root.html.heex` file.
After Commit
// If you want to use Phoenix channels, run `mix help phx.gen.channel`
// to get started and then uncomment the line below.
// import "./user_socket.js"

// You can include dependencies in two ways.
//
// The simplest option is to put them in assets/vendor and
// import them using relative paths:
//
//     import "../vendor/some-package.js"
//
// Alternatively, you can `npm install some-package --prefix assets` and import
// them using a path starting with the package name:
//
//     import "some-package"
//
// If you have dependencies that try to import CSS, esbuild will generate a separate `app.css` file.
// To load it, simply add a second `<link>` to your `root.html.heex` file.

Permutation: html: false, live: false (Output unchanged)

Unified Output Diff
(No output changes)
Full Rendered Outputs (Before & After)
Before Commit
// If you want to use Phoenix channels, run `mix help phx.gen.channel`
// to get started and then uncomment the line below.
// import "./user_socket.js"

// You can include dependencies in two ways.
//
// The simplest option is to put them in assets/vendor and
// import them using relative paths:
//
//     import "../vendor/some-package.js"
//
// Alternatively, you can `npm install some-package --prefix assets` and import
// them using a path starting with the package name:
//
//     import "some-package"
//
// If you have dependencies that try to import CSS, esbuild will generate a separate `app.css` file.
// To load it, simply add a second `<link>` to your `root.html.heex` file.
After Commit
// If you want to use Phoenix channels, run `mix help phx.gen.channel`
// to get started and then uncomment the line below.
// import "./user_socket.js"

// You can include dependencies in two ways.
//
// The simplest option is to put them in assets/vendor and
// import them using relative paths:
//
//     import "../vendor/some-package.js"
//
// Alternatively, you can `npm install some-package --prefix assets` and import
// them using a path starting with the package name:
//
//     import "some-package"
//
// If you have dependencies that try to import CSS, esbuild will generate a separate `app.css` file.
// To load it, simply add a second `<link>` to your `root.html.heex` file.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Computed Outputs Across Conditional Permutations

Permutation: ecto: true (Output unchanged)

Unified Output Diff
(No output changes)
Full Rendered Outputs (Before & After)
Before Commit
## This is a PO Template file.
##
## `msgid`s here are often extracted from source code.
## Add new translations manually only if they're dynamic
## translations that can't be statically extracted.
##
## Run `mix gettext.extract` to bring this file up to
## date. Leave `msgstr`s empty as changing them here has no
## effect: edit them in PO (`.po`) files instead.
## From Ecto.Changeset.cast/4
msgid "can't be blank"
msgstr ""

## From Ecto.Changeset.unique_constraint/3
msgid "has already been taken"
msgstr ""

## From Ecto.Changeset.put_change/3
msgid "is invalid"
msgstr ""

## From Ecto.Changeset.validate_acceptance/3
msgid "must be accepted"
msgstr ""

## From Ecto.Changeset.validate_format/3
msgid "has invalid format"
msgstr ""

## From Ecto.Changeset.validate_subset/3
msgid "has an invalid entry"
msgstr ""

## From Ecto.Changeset.validate_exclusion/3
msgid "is reserved"
msgstr ""

## From Ecto.Changeset.validate_confirmation/3
msgid "does not match confirmation"
msgstr ""

## From Ecto.Changeset.no_assoc_constraint/3
msgid "is still associated with this entry"
msgstr ""

msgid "are still associated with this entry"
msgstr ""

## From Ecto.Changeset.validate_length/3
msgid "should have %{count} item(s)"
msgid_plural "should have %{count} item(s)"
msgstr[0] ""
msgstr[1] ""

msgid "should be %{count} character(s)"
msgid_plural "should be %{count} character(s)"
msgstr[0] ""
msgstr[1] ""

msgid "should be %{count} byte(s)"
msgid_plural "should be %{count} byte(s)"
msgstr[0] ""
msgstr[1] ""

msgid "should have at least %{count} item(s)"
msgid_plural "should have at least %{count} item(s)"
msgstr[0] ""
msgstr[1] ""

msgid "should be at least %{count} character(s)"
msgid_plural "should be at least %{count} character(s)"
msgstr[0] ""
msgstr[1] ""

msgid "should be at least %{count} byte(s)"
msgid_plural "should be at least %{count} byte(s)"
msgstr[0] ""
msgstr[1] ""

msgid "should have at most %{count} item(s)"
msgid_plural "should have at most %{count} item(s)"
msgstr[0] ""
msgstr[1] ""

msgid "should be at most %{count} character(s)"
msgid_plural "should be at most %{count} character(s)"
msgstr[0] ""
msgstr[1] ""

msgid "should be at most %{count} byte(s)"
msgid_plural "should be at most %{count} byte(s)"
msgstr[0] ""
msgstr[1] ""

## From Ecto.Changeset.validate_number/3
msgid "must be less than %{number}"
msgstr ""

msgid "must be greater than %{number}"
msgstr ""

msgid "must be less than or equal to %{number}"
msgstr ""

msgid "must be greater than or equal to %{number}"
msgstr ""

msgid "must be equal to %{number}"
msgstr ""
After Commit
## This is a PO Template file.
##
## `msgid`s here are often extracted from source code.
## Add new translations manually only if they're dynamic
## translations that can't be statically extracted.
##
## Run `mix gettext.extract` to bring this file up to
## date. Leave `msgstr`s empty as changing them here has no
## effect: edit them in PO (`.po`) files instead.
## From Ecto.Changeset.cast/4
msgid "can't be blank"
msgstr ""

## From Ecto.Changeset.unique_constraint/3
msgid "has already been taken"
msgstr ""

## From Ecto.Changeset.put_change/3
msgid "is invalid"
msgstr ""

## From Ecto.Changeset.validate_acceptance/3
msgid "must be accepted"
msgstr ""

## From Ecto.Changeset.validate_format/3
msgid "has invalid format"
msgstr ""

## From Ecto.Changeset.validate_subset/3
msgid "has an invalid entry"
msgstr ""

## From Ecto.Changeset.validate_exclusion/3
msgid "is reserved"
msgstr ""

## From Ecto.Changeset.validate_confirmation/3
msgid "does not match confirmation"
msgstr ""

## From Ecto.Changeset.no_assoc_constraint/3
msgid "is still associated with this entry"
msgstr ""

msgid "are still associated with this entry"
msgstr ""

## From Ecto.Changeset.validate_length/3
msgid "should have %{count} item(s)"
msgid_plural "should have %{count} item(s)"
msgstr[0] ""
msgstr[1] ""

msgid "should be %{count} character(s)"
msgid_plural "should be %{count} character(s)"
msgstr[0] ""
msgstr[1] ""

msgid "should be %{count} byte(s)"
msgid_plural "should be %{count} byte(s)"
msgstr[0] ""
msgstr[1] ""

msgid "should have at least %{count} item(s)"
msgid_plural "should have at least %{count} item(s)"
msgstr[0] ""
msgstr[1] ""

msgid "should be at least %{count} character(s)"
msgid_plural "should be at least %{count} character(s)"
msgstr[0] ""
msgstr[1] ""

msgid "should be at least %{count} byte(s)"
msgid_plural "should be at least %{count} byte(s)"
msgstr[0] ""
msgstr[1] ""

msgid "should have at most %{count} item(s)"
msgid_plural "should have at most %{count} item(s)"
msgstr[0] ""
msgstr[1] ""

msgid "should be at most %{count} character(s)"
msgid_plural "should be at most %{count} character(s)"
msgstr[0] ""
msgstr[1] ""

msgid "should be at most %{count} byte(s)"
msgid_plural "should be at most %{count} byte(s)"
msgstr[0] ""
msgstr[1] ""

## From Ecto.Changeset.validate_number/3
msgid "must be less than %{number}"
msgstr ""

msgid "must be greater than %{number}"
msgstr ""

msgid "must be less than or equal to %{number}"
msgstr ""

msgid "must be greater than or equal to %{number}"
msgstr ""

msgid "must be equal to %{number}"
msgstr ""

Permutation: ecto: false (Output changed by commit)

Unified Output Diff
--- Before
+++ After
@@ -7,4 +7,3 @@
 ## Run `mix gettext.extract` to bring this file up to
 ## date. Leave `msgstr`s empty as changing them here has no
 ## effect: edit them in PO (`.po`) files instead.
-
Full Rendered Outputs (Before & After)
Before Commit
## This is a PO Template file.
##
## `msgid`s here are often extracted from source code.
## Add new translations manually only if they're dynamic
## translations that can't be statically extracted.
##
## Run `mix gettext.extract` to bring this file up to
## date. Leave `msgstr`s empty as changing them here has no
## effect: edit them in PO (`.po`) files instead.
After Commit
## This is a PO Template file.
##
## `msgid`s here are often extracted from source code.
## Add new translations manually only if they're dynamic
## translations that can't be statically extracted.
##
## Run `mix gettext.extract` to bring this file up to
## date. Leave `msgstr`s empty as changing them here has no
## effect: edit them in PO (`.po`) files instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Computed Outputs Across Conditional Permutations

Permutation: (javascript or css): true, sqlite3: true (Output changed by commit)

Unified Output Diff
--- Before
+++ After
@@ -38,4 +38,3 @@
 # Database files
 *.db
 *.db-*
-
Full Rendered Outputs (Before & After)
Before Commit
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Temporary files, for example, from tests.
/tmp/

# Ignore package tarball (built via "mix hex.build").
my_app-*.tar

# Ignore assets that are produced by build tools.
/priv/static/assets/

# Ignore digested assets cache.
/priv/static/cache_manifest.json

# In case you use Node.js/npm, you want to ignore these.
npm-debug.log
/assets/node_modules/

# Database files
*.db
*.db-*

After Commit
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Temporary files, for example, from tests.
/tmp/

# Ignore package tarball (built via "mix hex.build").
my_app-*.tar

# Ignore assets that are produced by build tools.
/priv/static/assets/

# Ignore digested assets cache.
/priv/static/cache_manifest.json

# In case you use Node.js/npm, you want to ignore these.
npm-debug.log
/assets/node_modules/

# Database files
*.db
*.db-*

Permutation: (javascript or css): true, sqlite3: false (Output changed by commit)

Unified Output Diff
--- Before
+++ After
@@ -34,4 +34,3 @@
 # In case you use Node.js/npm, you want to ignore these.
 npm-debug.log
 /assets/node_modules/
-
Full Rendered Outputs (Before & After)
Before Commit
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Temporary files, for example, from tests.
/tmp/

# Ignore package tarball (built via "mix hex.build").
my_app-*.tar

# Ignore assets that are produced by build tools.
/priv/static/assets/

# Ignore digested assets cache.
/priv/static/cache_manifest.json

# In case you use Node.js/npm, you want to ignore these.
npm-debug.log
/assets/node_modules/

After Commit
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Temporary files, for example, from tests.
/tmp/

# Ignore package tarball (built via "mix hex.build").
my_app-*.tar

# Ignore assets that are produced by build tools.
/priv/static/assets/

# Ignore digested assets cache.
/priv/static/cache_manifest.json

# In case you use Node.js/npm, you want to ignore these.
npm-debug.log
/assets/node_modules/

Permutation: (javascript or css): false, sqlite3: true (Output changed by commit)

Unified Output Diff
--- Before
+++ After
@@ -28,4 +28,3 @@
 # Database files
 *.db
 *.db-*
-
Full Rendered Outputs (Before & After)
Before Commit
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Temporary files, for example, from tests.
/tmp/

# Ignore package tarball (built via "mix hex.build").
my_app-*.tar

# Database files
*.db
*.db-*

After Commit
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Temporary files, for example, from tests.
/tmp/

# Ignore package tarball (built via "mix hex.build").
my_app-*.tar

# Database files
*.db
*.db-*

Permutation: (javascript or css): false, sqlite3: false (Output changed by commit)

Unified Output Diff
--- Before
+++ After
@@ -24,4 +24,3 @@
 
 # Ignore package tarball (built via "mix hex.build").
 my_app-*.tar
-
Full Rendered Outputs (Before & After)
Before Commit
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Temporary files, for example, from tests.
/tmp/

# Ignore package tarball (built via "mix hex.build").
my_app-*.tar

After Commit
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Temporary files, for example, from tests.
/tmp/

# Ignore package tarball (built via "mix hex.build").
my_app-*.tar

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Computed Outputs Across Conditional Permutations

Permutation: (javascript or css): true, sqlite3: true (Output changed by commit)

Unified Output Diff
--- Before
+++ After
@@ -38,4 +38,3 @@
 # Database files
 *.db
 *.db-*
-
Full Rendered Outputs (Before & After)
Before Commit
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Temporary files, for example, from tests.
/tmp/

# Ignore package tarball (built via "mix hex.build").
my_app_web-*.tar

# Ignore assets that are produced by build tools.
/priv/static/assets/

# Ignore digested assets cache.
/priv/static/cache_manifest.json

# In case you use Node.js/npm, you want to ignore these.
npm-debug.log
/assets/node_modules/

# Database files
*.db
*.db-*

After Commit
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Temporary files, for example, from tests.
/tmp/

# Ignore package tarball (built via "mix hex.build").
my_app_web-*.tar

# Ignore assets that are produced by build tools.
/priv/static/assets/

# Ignore digested assets cache.
/priv/static/cache_manifest.json

# In case you use Node.js/npm, you want to ignore these.
npm-debug.log
/assets/node_modules/

# Database files
*.db
*.db-*

Permutation: (javascript or css): true, sqlite3: false (Output changed by commit)

Unified Output Diff
--- Before
+++ After
@@ -34,4 +34,3 @@
 # In case you use Node.js/npm, you want to ignore these.
 npm-debug.log
 /assets/node_modules/
-
Full Rendered Outputs (Before & After)
Before Commit
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Temporary files, for example, from tests.
/tmp/

# Ignore package tarball (built via "mix hex.build").
my_app_web-*.tar

# Ignore assets that are produced by build tools.
/priv/static/assets/

# Ignore digested assets cache.
/priv/static/cache_manifest.json

# In case you use Node.js/npm, you want to ignore these.
npm-debug.log
/assets/node_modules/

After Commit
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Temporary files, for example, from tests.
/tmp/

# Ignore package tarball (built via "mix hex.build").
my_app_web-*.tar

# Ignore assets that are produced by build tools.
/priv/static/assets/

# Ignore digested assets cache.
/priv/static/cache_manifest.json

# In case you use Node.js/npm, you want to ignore these.
npm-debug.log
/assets/node_modules/

Permutation: (javascript or css): false, sqlite3: true (Output changed by commit)

Unified Output Diff
--- Before
+++ After
@@ -28,4 +28,3 @@
 # Database files
 *.db
 *.db-*
-
Full Rendered Outputs (Before & After)
Before Commit
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Temporary files, for example, from tests.
/tmp/

# Ignore package tarball (built via "mix hex.build").
my_app_web-*.tar

# Database files
*.db
*.db-*

After Commit
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Temporary files, for example, from tests.
/tmp/

# Ignore package tarball (built via "mix hex.build").
my_app_web-*.tar

# Database files
*.db
*.db-*

Permutation: (javascript or css): false, sqlite3: false (Output changed by commit)

Unified Output Diff
--- Before
+++ After
@@ -24,4 +24,3 @@
 
 # Ignore package tarball (built via "mix hex.build").
 my_app_web-*.tar
-
Full Rendered Outputs (Before & After)
Before Commit
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Temporary files, for example, from tests.
/tmp/

# Ignore package tarball (built via "mix hex.build").
my_app_web-*.tar

After Commit
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Temporary files, for example, from tests.
/tmp/

# Ignore package tarball (built via "mix hex.build").
my_app_web-*.tar

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Computed Outputs Across Conditional Permutations

Permutation: mailer: true, html: true (Output changed by commit)

Unified Output Diff
--- Before
+++ After
@@ -16,7 +16,7 @@
 # Enable helpful, but potentially expensive runtime checks
 config :phoenix_live_view,
   enable_expensive_runtime_checks: true
-  
+
 # Sort query params output of verified routes for robust url comparisons
 config :phoenix,
-  sort_verified_routes_query_params: true
\ No newline at end of file
+  sort_verified_routes_query_params: true
Full Rendered Outputs (Before & After)
Before Commit
import Config

# Print only warnings and errors during test
config :logger, level: :warning

# In test we don't send emails
config :my_app, MyApp.Mailer,
  adapter: Swoosh.Adapters.Test

# Disable swoosh api client as it is only required for production adapters
config :swoosh, :api_client, false

# Initialize plugs at runtime for faster test compilation
config :phoenix, :plug_init_mode, :runtime

# Enable helpful, but potentially expensive runtime checks
config :phoenix_live_view,
  enable_expensive_runtime_checks: true
  
# Sort query params output of verified routes for robust url comparisons
config :phoenix,
  sort_verified_routes_query_params: true```

##### After Commit
```elixir
import Config

# Print only warnings and errors during test
config :logger, level: :warning

# In test we don't send emails
config :my_app, MyApp.Mailer,
  adapter: Swoosh.Adapters.Test

# Disable swoosh api client as it is only required for production adapters
config :swoosh, :api_client, false

# Initialize plugs at runtime for faster test compilation
config :phoenix, :plug_init_mode, :runtime

# Enable helpful, but potentially expensive runtime checks
config :phoenix_live_view,
  enable_expensive_runtime_checks: true

# Sort query params output of verified routes for robust url comparisons
config :phoenix,
  sort_verified_routes_query_params: true

Permutation: mailer: true, html: false (Output changed by commit)

Unified Output Diff
--- Before
+++ After
@@ -12,7 +12,7 @@
 
 # Initialize plugs at runtime for faster test compilation
 config :phoenix, :plug_init_mode, :runtime
-  
+
 # Sort query params output of verified routes for robust url comparisons
 config :phoenix,
-  sort_verified_routes_query_params: true
\ No newline at end of file
+  sort_verified_routes_query_params: true
Full Rendered Outputs (Before & After)
Before Commit
import Config

# Print only warnings and errors during test
config :logger, level: :warning

# In test we don't send emails
config :my_app, MyApp.Mailer,
  adapter: Swoosh.Adapters.Test

# Disable swoosh api client as it is only required for production adapters
config :swoosh, :api_client, false

# Initialize plugs at runtime for faster test compilation
config :phoenix, :plug_init_mode, :runtime
  
# Sort query params output of verified routes for robust url comparisons
config :phoenix,
  sort_verified_routes_query_params: true```

##### After Commit
```elixir
import Config

# Print only warnings and errors during test
config :logger, level: :warning

# In test we don't send emails
config :my_app, MyApp.Mailer,
  adapter: Swoosh.Adapters.Test

# Disable swoosh api client as it is only required for production adapters
config :swoosh, :api_client, false

# Initialize plugs at runtime for faster test compilation
config :phoenix, :plug_init_mode, :runtime

# Sort query params output of verified routes for robust url comparisons
config :phoenix,
  sort_verified_routes_query_params: true

Permutation: mailer: false, html: true (Output changed by commit)

Unified Output Diff
--- Before
+++ After
@@ -9,7 +9,7 @@
 # Enable helpful, but potentially expensive runtime checks
 config :phoenix_live_view,
   enable_expensive_runtime_checks: true
-  
+
 # Sort query params output of verified routes for robust url comparisons
 config :phoenix,
-  sort_verified_routes_query_params: true
\ No newline at end of file
+  sort_verified_routes_query_params: true
Full Rendered Outputs (Before & After)
Before Commit
import Config

# Print only warnings and errors during test
config :logger, level: :warning

# Initialize plugs at runtime for faster test compilation
config :phoenix, :plug_init_mode, :runtime

# Enable helpful, but potentially expensive runtime checks
config :phoenix_live_view,
  enable_expensive_runtime_checks: true
  
# Sort query params output of verified routes for robust url comparisons
config :phoenix,
  sort_verified_routes_query_params: true```

##### After Commit
```elixir
import Config

# Print only warnings and errors during test
config :logger, level: :warning

# Initialize plugs at runtime for faster test compilation
config :phoenix, :plug_init_mode, :runtime

# Enable helpful, but potentially expensive runtime checks
config :phoenix_live_view,
  enable_expensive_runtime_checks: true

# Sort query params output of verified routes for robust url comparisons
config :phoenix,
  sort_verified_routes_query_params: true

Permutation: mailer: false, html: false (Output changed by commit)

Unified Output Diff
--- Before
+++ After
@@ -5,7 +5,7 @@
 
 # Initialize plugs at runtime for faster test compilation
 config :phoenix, :plug_init_mode, :runtime
-  
+
 # Sort query params output of verified routes for robust url comparisons
 config :phoenix,
-  sort_verified_routes_query_params: true
\ No newline at end of file
+  sort_verified_routes_query_params: true
Full Rendered Outputs (Before & After)
Before Commit
import Config

# Print only warnings and errors during test
config :logger, level: :warning

# Initialize plugs at runtime for faster test compilation
config :phoenix, :plug_init_mode, :runtime
  
# Sort query params output of verified routes for robust url comparisons
config :phoenix,
  sort_verified_routes_query_params: true```

##### After Commit
```elixir
import Config

# Print only warnings and errors during test
config :logger, level: :warning

# Initialize plugs at runtime for faster test compilation
config :phoenix, :plug_init_mode, :runtime

# Sort query params output of verified routes for robust url comparisons
config :phoenix,
  sort_verified_routes_query_params: true

Comment on lines +11 to +12
<%= if (fields = Mix.Phoenix.Schema.format_fields_for_schema(schema)) != "" do %><%= fields %>
<% end %><%= for {_, k, _, _} <- schema.assocs do %> field <%= inspect k %>, <%= if schema.binary_id do %>:binary_id<% else %>:id<% end %>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering the precedent of format_fields_for_schema/1, we can simplify this particular template with another helper format_schema_body/2 that takes care of joining the multiple components without this line break dance in the template.

I can volunteer to revisit #6015 as a follow up, essentially applying Code.format_string!/2 to generated Elixir code, which would handle the remaining cases like long lines.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Computed Outputs Across Conditional Permutations

Permutation: With fields (title:string), no assocs (Output unchanged)

Unified Output Diff
(No output changes)
Full Rendered Outputs (Before & After)
Before Commit
defmodule Phoenix.Blog.Post do
  use Ecto.Schema
  import Ecto.Changeset

  schema "posts" do
    field :title, :string

    timestamps()
  end

  @doc false
  def changeset(post, attrs) do
    post
    |> cast(attrs, [:title])
    |> validate_required([:title])
  end
end
After Commit
defmodule Phoenix.Blog.Post do
  use Ecto.Schema
  import Ecto.Changeset

  schema "posts" do
    field :title, :string

    timestamps()
  end

  @doc false
  def changeset(post, attrs) do
    post
    |> cast(attrs, [:title])
    |> validate_required([:title])
  end
end

Permutation: No fields, no assocs (Output changed by commit)

Unified Output Diff
--- Before
+++ After
@@ -3,8 +3,6 @@
   import Ecto.Changeset
 
   schema "posts" do
-
-
     timestamps()
   end
 
Full Rendered Outputs (Before & After)
Before Commit
defmodule Phoenix.Blog.Post do
  use Ecto.Schema
  import Ecto.Changeset

  schema "posts" do


    timestamps()
  end

  @doc false
  def changeset(post, attrs) do
    post
    |> cast(attrs, [])
    |> validate_required([])
  end
end
After Commit
defmodule Phoenix.Blog.Post do
  use Ecto.Schema
  import Ecto.Changeset

  schema "posts" do
    timestamps()
  end

  @doc false
  def changeset(post, attrs) do
    post
    |> cast(attrs, [])
    |> validate_required([])
  end
end

Permutation: No fields, with assoc (post_id:references) (Output changed by commit)

Unified Output Diff
--- Before
+++ After
@@ -3,7 +3,6 @@
   import Ecto.Changeset
 
   schema "comments" do
-
     field :post_id, :id
 
     timestamps()
Full Rendered Outputs (Before & After)
Before Commit
defmodule Phoenix.Blog.Comment do
  use Ecto.Schema
  import Ecto.Changeset

  schema "comments" do

    field :post_id, :id

    timestamps()
  end

  @doc false
  def changeset(comment, attrs) do
    comment
    |> cast(attrs, [])
    |> validate_required([])
  end
end
After Commit
defmodule Phoenix.Blog.Comment do
  use Ecto.Schema
  import Ecto.Changeset

  schema "comments" do
    field :post_id, :id

    timestamps()
  end

  @doc false
  def changeset(comment, attrs) do
    comment
    |> cast(attrs, [])
    |> validate_required([])
  end
end

Permutation: With fields (title:string) and assoc (Output changed by commit)

Unified Output Diff
--- Before
+++ After
@@ -3,7 +3,6 @@
   import Ecto.Changeset
 
   schema "comments" do
-
     field :post_id, :id
 
     timestamps()
Full Rendered Outputs (Before & After)
Before Commit
defmodule Phoenix.Blog.Comment do
  use Ecto.Schema
  import Ecto.Changeset

  schema "comments" do

    field :post_id, :id

    timestamps()
  end

  @doc false
  def changeset(comment, attrs) do
    comment
    |> cast(attrs, [])
    |> validate_required([])
  end
end
After Commit
defmodule Phoenix.Blog.Comment do
  use Ecto.Schema
  import Ecto.Changeset

  schema "comments" do
    field :post_id, :id

    timestamps()
  end

  @doc false
  def changeset(comment, attrs) do
    comment
    |> cast(attrs, [])
    |> validate_required([])
  end
end

Comment on lines +9 to +10
## Channels<%= if existing_channel do %>

channel "<%= existing_channel[:singular] %>:*", <%= existing_channel[:module] %>Channel
<% else %>
channel "<%= existing_channel[:singular] %>:*", <%= existing_channel[:module] %>Channel<% else %>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. This is one weird case.

The generated code is an Elixir module-level comment before the channel macro call.

  1. It seems non-standard that it has a double hash-sign.
  2. We don't do such sectional comments in Endpoint sockets or plugs.

I'm pro removing the comment altogether. Will do in a separate commit so we can evaluate and decide whether to keep or drop.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Computed Outputs Across Conditional Permutations

Permutation: existing_channel present (Output changed by commit)

Unified Output Diff
--- Before
+++ After
@@ -6,8 +6,6 @@
   # It's possible to control the websocket connection and
   # assign values that can be accessed by your channel topics.
 
-  ## Channels
-
   channel "user:*", UserChannel
 
   # Socket params are passed from the client and can
Full Rendered Outputs (Before & After)
Before Commit
defmodule UserSocket do
  use Phoenix.Socket

  # A Socket handler
  #
  # It's possible to control the websocket connection and
  # assign values that can be accessed by your channel topics.

  ## Channels

  channel "user:*", UserChannel

  # Socket params are passed from the client and can
  # be used to verify and authenticate a user. After
  # verification, you can put default assigns into
  # the socket that will be set for all channels, ie
  #
  #     {:ok, assign(socket, :user_id, verified_user_id)}
  #
  # To deny connection, return `:error` or `{:error, term}`. To control the
  # response the client receives in that case, [define an error handler in the
  # websocket
  # configuration](https://phoenix.hexdocs.pm/Phoenix.Endpoint.html#socket/3-websocket-configuration).
  #
  # See `Phoenix.Token` documentation for examples in
  # performing token verification on connect.
  @impl true
  def connect(_params, socket, _connect_info) do
    {:ok, socket}
  end

  # Socket IDs are topics that allow you to identify all sockets for a given user:
  #
  #     def id(socket), do: "user_socket:#{socket.assigns.user_id}"
  #
  # Would allow you to broadcast a "disconnect" event and terminate
  # all active sockets and channels for a given user:
  #
  #     UserWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
  #
  # Returning `nil` makes this socket anonymous.
  @impl true
  def id(_socket), do: nil
end
After Commit
defmodule UserSocket do
  use Phoenix.Socket

  # A Socket handler
  #
  # It's possible to control the websocket connection and
  # assign values that can be accessed by your channel topics.

  channel "user:*", UserChannel

  # Socket params are passed from the client and can
  # be used to verify and authenticate a user. After
  # verification, you can put default assigns into
  # the socket that will be set for all channels, ie
  #
  #     {:ok, assign(socket, :user_id, verified_user_id)}
  #
  # To deny connection, return `:error` or `{:error, term}`. To control the
  # response the client receives in that case, [define an error handler in the
  # websocket
  # configuration](https://phoenix.hexdocs.pm/Phoenix.Endpoint.html#socket/3-websocket-configuration).
  #
  # See `Phoenix.Token` documentation for examples in
  # performing token verification on connect.
  @impl true
  def connect(_params, socket, _connect_info) do
    {:ok, socket}
  end

  # Socket IDs are topics that allow you to identify all sockets for a given user:
  #
  #     def id(socket), do: "user_socket:#{socket.assigns.user_id}"
  #
  # Would allow you to broadcast a "disconnect" event and terminate
  # all active sockets and channels for a given user:
  #
  #     UserWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
  #
  # Returning `nil` makes this socket anonymous.
  @impl true
  def id(_socket), do: nil
end

Permutation: existing_channel nil (Output changed by commit)

Unified Output Diff
--- Before
+++ After
@@ -6,7 +6,6 @@
   # It's possible to control the websocket connection and
   # assign values that can be accessed by your channel topics.
 
-  ## Channels
   # Uncomment the following line to define a "room:*" topic
   # pointing to the `UserWeb.RoomChannel`:
   #
@@ -19,7 +18,6 @@
   # See the [`Channels guide`](https://phoenix.hexdocs.pm/channels.html)
   # for further details.
 
-
   # Socket params are passed from the client and can
   # be used to verify and authenticate a user. After
   # verification, you can put default assigns into
Full Rendered Outputs (Before & After)
Before Commit
defmodule UserSocket do
  use Phoenix.Socket

  # A Socket handler
  #
  # It's possible to control the websocket connection and
  # assign values that can be accessed by your channel topics.

  ## Channels
  # Uncomment the following line to define a "room:*" topic
  # pointing to the `UserWeb.RoomChannel`:
  #
  # channel "room:*", UserWeb.RoomChannel
  #
  # To create a channel file, use the mix task:
  #
  #     mix phx.gen.channel Room
  #
  # See the [`Channels guide`](https://phoenix.hexdocs.pm/channels.html)
  # for further details.


  # Socket params are passed from the client and can
  # be used to verify and authenticate a user. After
  # verification, you can put default assigns into
  # the socket that will be set for all channels, ie
  #
  #     {:ok, assign(socket, :user_id, verified_user_id)}
  #
  # To deny connection, return `:error` or `{:error, term}`. To control the
  # response the client receives in that case, [define an error handler in the
  # websocket
  # configuration](https://phoenix.hexdocs.pm/Phoenix.Endpoint.html#socket/3-websocket-configuration).
  #
  # See `Phoenix.Token` documentation for examples in
  # performing token verification on connect.
  @impl true
  def connect(_params, socket, _connect_info) do
    {:ok, socket}
  end

  # Socket IDs are topics that allow you to identify all sockets for a given user:
  #
  #     def id(socket), do: "user_socket:#{socket.assigns.user_id}"
  #
  # Would allow you to broadcast a "disconnect" event and terminate
  # all active sockets and channels for a given user:
  #
  #     UserWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
  #
  # Returning `nil` makes this socket anonymous.
  @impl true
  def id(_socket), do: nil
end
After Commit
defmodule UserSocket do
  use Phoenix.Socket

  # A Socket handler
  #
  # It's possible to control the websocket connection and
  # assign values that can be accessed by your channel topics.

  # Uncomment the following line to define a "room:*" topic
  # pointing to the `UserWeb.RoomChannel`:
  #
  # channel "room:*", UserWeb.RoomChannel
  #
  # To create a channel file, use the mix task:
  #
  #     mix phx.gen.channel Room
  #
  # See the [`Channels guide`](https://phoenix.hexdocs.pm/channels.html)
  # for further details.

  # Socket params are passed from the client and can
  # be used to verify and authenticate a user. After
  # verification, you can put default assigns into
  # the socket that will be set for all channels, ie
  #
  #     {:ok, assign(socket, :user_id, verified_user_id)}
  #
  # To deny connection, return `:error` or `{:error, term}`. To control the
  # response the client receives in that case, [define an error handler in the
  # websocket
  # configuration](https://phoenix.hexdocs.pm/Phoenix.Endpoint.html#socket/3-websocket-configuration).
  #
  # See `Phoenix.Token` documentation for examples in
  # performing token verification on connect.
  @impl true
  def connect(_params, socket, _connect_info) do
    {:ok, socket}
  end

  # Socket IDs are topics that allow you to identify all sockets for a given user:
  #
  #     def id(socket), do: "user_socket:#{socket.assigns.user_id}"
  #
  # Would allow you to broadcast a "disconnect" event and terminate
  # all active sockets and channels for a given user:
  #
  #     UserWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
  #
  # Returning `nil` makes this socket anonymous.
  @impl true
  def id(_socket), do: nil
end

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Computed Outputs Across Conditional Permutations

Permutation: Full-stack project (Representative of all 16 flag permutations) (Output changed by commit)

Unified Output Diff
--- Before
+++ After
@@ -43,7 +43,6 @@
 - Ensure **clean typography, spacing, and layout balance** for a refined, premium look
 - Focus on **delightful details** like hover effects, loading states, and smooth page transitions
 
-
 <!-- usage-rules-start -->
 
 <!-- phoenix:elixir-start -->
@@ -446,4 +445,4 @@
 - **Never** use `<.form let={f} ...>` in the template, instead **always use `<.form for={@form} ...>`**, then drive all form references from the form assign as in `@form[:field]`. The UI should **always** be driven by a `to_form/2` assigned in the LiveView module that is derived from a changeset
 <!-- phoenix:liveview-end -->
 
-<!-- usage-rules-end -->
\ No newline at end of file
+<!-- usage-rules-end -->

Full Rendered Outputs (Before & After) omitted for brevity.

@rhcarvalho

Copy link
Copy Markdown
Contributor Author

Forgot to post on the previous comment. The diff generation for manual review was generated with an AI-assisted one-off script, included here for reference/transpareny.

generate_template_changes_review.exs
defmodule GenerateTemplateChangesReview do
  @doc """
  Evaluates template files changed between HEAD~1 and the working tree,
  computes all conditional branch outputs and unified diffs, and writes
  the summary to `template_changes_review.md`.

  Usage:
      mix run generate_template_changes_review.exs
  """

  @target_md_path "template_changes_review.md"

  @files [
    {"installer/templates/phx_assets/app.js.eex", :app_js},
    {"installer/templates/phx_gettext/errors.pot.eex", :errors_pot},
    {"installer/templates/phx_single/gitignore.eex", :single_gitignore},
    {"installer/templates/phx_static/default.css", :default_css},
    {"installer/templates/phx_umbrella/apps/app_name_web/gitignore.eex", :umbrella_web_gitignore},
    {"installer/templates/phx_umbrella/config/test.exs.eex", :umbrella_test_exs},
    {"installer/templates/phx_umbrella/gitignore.eex", :umbrella_gitignore},
    {"priv/templates/phx.gen.schema/schema.ex.eex", :schema_ex},
    {"priv/templates/phx.gen.socket/socket.ex.eex", :socket_ex},
    {"AGENTS.md (generated)", :agents_md}
  ]

  def run do
    base_ref = get_base_ref()
    IO.puts("Computing template branch permutations between #{base_ref} and working tree...")

    rules_now = load_rules_now()
    new_rules_now = load_new_rules_now()
    rules_base = load_rules_base(base_ref)
    new_rules_base = load_new_rules_base(base_ref)

    data =
      Enum.map(@files, fn {rel_path, key} ->
        perms = permutations_for(key)

        eval_perms =
          Enum.map(perms, fn {desc, binding} ->
            out_before = eval(key, rules_base, new_rules_base, base_ref, rel_path, binding, :before)
            out_after = eval(key, rules_now, new_rules_now, base_ref, rel_path, binding, :after)
            diff_text = compute_diff(out_before, out_after)

            %{
              "description" => desc,
              "before" => out_before,
              "after" => out_after,
              "diff" => diff_text,
              "changed" => out_before != out_after
            }
          end)

        diff_str =
          if key == :agents_md do
            {out, _} = System.cmd("git", ["diff", base_ref, "--", "usage-rules/", "installer/templates/usage-rules/"])
            out
          else
            {out, _} = System.cmd("git", ["diff", base_ref, "--", rel_path])
            out
          end

        %{
          "file_path" => rel_path,
          "diff" => diff_str,
          "permutations" => eval_perms
        }
      end)

    doc = [
      "# Template Changes Review: Conditional Branch Analysis\n",
      "> [!NOTE]\n",
      "> **How to Regenerate This Document**\n",
      "> You can regenerate this document at any time by running:\n",
      "> ```bash\n",
      "> mix run generate_template_changes_review.exs\n",
      "> ```\n",
      "> The script evaluates all changed templates against `HEAD~1` across all conditional permutations.\n\n",
      "This document analyzes all template modifications introduced in commit `b679931f0` (*\"Ensure single trailing newline in generated files\"*). For each modified template, it details the template diff and the exact computed outputs across all permutations of conditional branches before and after the commit, along with a unified diff of the rendered output.\n",
      "## Index / Summary\n",
      "| Template File | Conditionals Analyzed | Permutations Count | Behavior Changed? |",
      "| :--- | :--- | :---: | :---: |"
    ]

    summary_rows =
      Enum.map(data, fn item ->
        file = item["file_path"]
        perms = item["permutations"]
        count = length(perms)

        cond_desc =
          case file do
            "installer/templates/phx_assets/app.js.eex" -> "`@html`, `@live`"
            "installer/templates/phx_gettext/errors.pot.eex" -> "`@ecto`"
            "installer/templates/phx_single/gitignore.eex" -> "`@javascript or @css`, `@adapter_app`"
            "installer/templates/phx_static/default.css" -> "*(None / Static asset)*"
            "installer/templates/phx_umbrella/apps/app_name_web/gitignore.eex" -> "`@javascript or @css`, `@adapter_app`"
            "installer/templates/phx_umbrella/config/test.exs.eex" -> "`@mailer`, `@html`"
            "installer/templates/phx_umbrella/gitignore.eex" -> "`@adapter_app`"
            "priv/templates/phx.gen.schema/schema.ex.eex" -> "`Mix.Phoenix.Schema.format_fields_for_schema(schema)`"
            "priv/templates/phx.gen.socket/socket.ex.eex" -> "`existing_channel`"
            "AGENTS.md (generated)" -> "`@ecto`, `@html`, `@live`, `@javascript and @css` (all 16 permutations share identical diff structure)"
          end

        any_changed = Enum.any?(perms, & &1["changed"])
        status = if any_changed, do: "Yes (Whitespace / Newlines cleaned)", else: "No"

        "| [`#{file}`](#user-content-#{anchor_id(file)}) | #{cond_desc} | #{count} | #{status} |"
      end)

    doc = doc ++ summary_rows ++ ["\n---\n"]

    file_sections =
      Enum.map(data, fn item ->
        file = item["file_path"]
        diff = item["diff"]
        perms = item["permutations"]
        lang = lang_for(file)

        section = [
          "## #{file}\n",
          "### Commit Change (Diff)\n",
          "```diff\n#{diff}```\n",
          "### Computed Outputs Across Conditional Permutations\n"
        ]

        perm_blocks =
          Enum.map(perms, fn p ->
            desc = p["description"]
            before_val = p["before"]
            after_val = p["after"]
            diff_val = p["diff"]
            changed = p["changed"]

            change_note = if changed, do: "*(Output changed by commit)*", else: "*(Output unchanged)*"

            """
            #### Permutation: #{desc} #{change_note}

            ##### Unified Output Diff
            ```diff
            #{diff_val}```

            <details>
            <summary>Full Rendered Outputs (Before &amp; After)</summary>

            ##### Before Commit
            ```#{lang}
            #{before_val}```

            ##### After Commit
            ```#{lang}
            #{after_val}```

            </details>
            """
          end)

        Enum.join(section ++ perm_blocks, "\n") <> "\n---\n"
      end)

    final_content = Enum.join(doc ++ file_sections, "\n")
    File.write!(@target_md_path, final_content)
    IO.puts("Successfully generated #{@target_md_path}!")
  end

  defp compute_diff(before_str, after_str) do
    if before_str == after_str do
      "(No output changes)\n"
    else
      tmp_before = Path.join(System.tmp_dir!(), "before_#{System.unique_integer([:positive])}.txt")
      tmp_after = Path.join(System.tmp_dir!(), "after_#{System.unique_integer([:positive])}.txt")
      File.write!(tmp_before, before_str)
      File.write!(tmp_after, after_str)

      {diff_out, _exit} = System.cmd("diff", ["-u", "--label", "Before", "--label", "After", tmp_before, tmp_after])

      File.rm(tmp_before)
      File.rm(tmp_after)
      diff_out
    end
  end

  defp eval(:agents_md, rules_now, new_rules_now, _base_ref, _rel_path, binding, :after) do
    render_agents_md_after(rules_now, new_rules_now, binding)
  end

  defp eval(:agents_md, _rules_now, _new_rules_now, base_ref, _rel_path, binding, :before) do
    new_rules_base = load_new_rules_base(base_ref)
    rules_base = load_rules_base(base_ref)
    render_agents_md_before(rules_base, new_rules_base, binding)
  end

  defp eval(:default_css, _rules, _new_rules, base_ref, rel_path, _binding, :before) do
    {tpl, 0} = System.cmd("git", ["show", base_ref <> ":" <> rel_path])
    tpl
  end

  defp eval(:default_css, _rules, _new_rules, _base_ref, rel_path, _binding, :after) do
    File.read!(rel_path)
  end

  defp eval(:schema_ex, _rules, _new_rules, base_ref, rel_path, binding, :before) do
    {tpl, 0} = System.cmd("git", ["show", base_ref <> ":" <> rel_path])
    EEx.eval_string(tpl, binding)
  end

  defp eval(:schema_ex, _rules, _new_rules, _base_ref, rel_path, binding, :after) do
    tpl = File.read!(rel_path)
    EEx.eval_string(tpl, binding)
  end

  defp eval(:socket_ex, _rules, _new_rules, base_ref, rel_path, binding, :before) do
    {tpl, 0} = System.cmd("git", ["show", base_ref <> ":" <> rel_path])
    EEx.eval_string(tpl, binding)
  end

  defp eval(:socket_ex, _rules, _new_rules, _base_ref, rel_path, binding, :after) do
    tpl = File.read!(rel_path)
    EEx.eval_string(tpl, binding)
  end

  defp eval(_key, _rules, _new_rules, base_ref, rel_path, binding, :before) do
    {tpl, 0} = System.cmd("git", ["show", base_ref <> ":" <> rel_path])
    EEx.eval_string(tpl, assigns: binding)
  end

  defp eval(_key, _rules, _new_rules, _base_ref, rel_path, binding, :after) do
    tpl = File.read!(rel_path)
    EEx.eval_string(tpl, assigns: binding)
  end

  defp permutations_for(:app_js) do
    base = [phoenix_js_path: "phoenix", web_app_name: "my_app_web"]
    [
      {"html: true, live: true", Keyword.merge(base, [html: true, live: true, live_comment: ""])},
      {"html: true, live: false", Keyword.merge(base, [html: true, live: false, live_comment: "// "])},
      {"html: false, live: true", Keyword.merge(base, [html: false, live: true, live_comment: ""])},
      {"html: false, live: false", Keyword.merge(base, [html: false, live: false, live_comment: "// "])}
    ]
  end

  defp permutations_for(:errors_pot) do
    [
      {"ecto: true", [ecto: true]},
      {"ecto: false", [ecto: false]}
    ]
  end

  defp permutations_for(:single_gitignore) do
    base = [app_name: "my_app"]
    [
      {"(javascript or css): true, sqlite3: true", Keyword.merge(base, [javascript: true, css: false, adapter_app: :ecto_sqlite3])},
      {"(javascript or css): true, sqlite3: false", Keyword.merge(base, [javascript: true, css: false, adapter_app: :ecto_postgres])},
      {"(javascript or css): false, sqlite3: true", Keyword.merge(base, [javascript: false, css: false, adapter_app: :ecto_sqlite3])},
      {"(javascript or css): false, sqlite3: false", Keyword.merge(base, [javascript: false, css: false, adapter_app: :ecto_postgres])}
    ]
  end

  defp permutations_for(:default_css) do
    [{"(static file, no conditionals)", []}]
  end

  defp permutations_for(:umbrella_web_gitignore) do
    base = [web_app_name: "my_app_web"]
    [
      {"(javascript or css): true, sqlite3: true", Keyword.merge(base, [javascript: true, css: false, adapter_app: :ecto_sqlite3])},
      {"(javascript or css): true, sqlite3: false", Keyword.merge(base, [javascript: true, css: false, adapter_app: :ecto_postgres])},
      {"(javascript or css): false, sqlite3: true", Keyword.merge(base, [javascript: false, css: false, adapter_app: :ecto_sqlite3])},
      {"(javascript or css): false, sqlite3: false", Keyword.merge(base, [javascript: false, css: false, adapter_app: :ecto_postgres])}
    ]
  end

  defp permutations_for(:umbrella_test_exs) do
    base = [app_name: "my_app", app_module: "MyApp", web_app_name: "my_app_web"]
    [
      {"mailer: true, html: true", Keyword.merge(base, [mailer: true, html: true])},
      {"mailer: true, html: false", Keyword.merge(base, [mailer: true, html: false])},
      {"mailer: false, html: true", Keyword.merge(base, [mailer: false, html: true])},
      {"mailer: false, html: false", Keyword.merge(base, [mailer: false, html: false])}
    ]
  end

  defp permutations_for(:umbrella_gitignore) do
    [
      {"sqlite3: true", [adapter_app: :ecto_sqlite3]},
      {"sqlite3: false", [adapter_app: :ecto_postgres]}
    ]
  end

  defp permutations_for(:schema_ex) do
    alias Mix.Phoenix.Schema
    s_with_fields = Schema.new("Blog.Post", "posts", ["title:string"], [])
    s_no_fields = Schema.new("Blog.Post", "posts", [], [])
    s_no_fields_assoc = Schema.new("Blog.Comment", "comments", ["post_id:references:posts"], [])
    s_empty_with_assoc = %{s_no_fields_assoc | types: %{}}

    [
      {"With fields (title:string), no assocs", [schema: s_with_fields, primary_key: :id, scope: nil]},
      {"No fields, no assocs", [schema: s_no_fields, primary_key: :id, scope: nil]},
      {"No fields, with assoc (post_id:references)", [schema: s_empty_with_assoc, primary_key: :id, scope: nil]},
      {"With fields (title:string) and assoc", [schema: s_no_fields_assoc, primary_key: :id, scope: nil]}
    ]
  end

  defp permutations_for(:socket_ex) do
    base = [module: "User", web_module: "UserWeb", endpoint_module: "UserWeb.Endpoint"]
    [
      {"existing_channel present", Keyword.merge(base, [existing_channel: [singular: "user", module: "User"]])},
      {"existing_channel nil", Keyword.merge(base, [existing_channel: nil])}
    ]
  end

  defp permutations_for(:agents_md) do
    [
      {"Full-stack project (Representative of all 16 flag permutations)",
       [html: true, live: true, ecto: true, javascript: true, css: true]}
    ]
  end

  defp render_agents_md_before(rules, new_project_rules, binding) do
    [
      new_project_rules["project.md"],
      new_project_rules["phoenix.md"],
      binding[:javascript] && binding[:css] && new_project_rules["assets.md"],
      "\n<!-- usage-rules-start -->",
      [
        "<!-- phoenix:elixir-start -->\n",
        rules["elixir.md"],
        "\n<!-- phoenix:elixir-end -->"
      ],
      [
        "<!-- phoenix:phoenix-start -->\n",
        rules["phoenix.md"],
        "\n<!-- phoenix:phoenix-end -->"
      ],
      binding[:ecto] &&
        [
          "<!-- phoenix:ecto-start -->\n",
          rules["ecto.md"],
          "\n<!-- phoenix:ecto-end -->"
        ],
      binding[:html] &&
        [
          "<!-- phoenix:html-start -->\n",
          rules["html.md"],
          "\n<!-- phoenix:html-end -->"
        ],
      binding[:live] &&
        [
          "<!-- phoenix:liveview-start -->\n",
          rules["liveview.md"],
          "\n<!-- phoenix:liveview-end -->"
        ],
      "<!-- usage-rules-end -->"
    ]
    |> Enum.reject(fn part -> part == nil or part == false end)
    |> Enum.intersperse("\n\n")
    |> IO.iodata_to_binary()
  end

  defp render_agents_md_after(rules, new_project_rules, binding) do
    [
      new_project_rules["project.md"],
      new_project_rules["phoenix.md"],
      binding[:javascript] && binding[:css] && new_project_rules["assets.md"],
      "<!-- usage-rules-start -->",
      [
        "<!-- phoenix:elixir-start -->\n",
        rules["elixir.md"],
        "\n<!-- phoenix:elixir-end -->"
      ],
      [
        "<!-- phoenix:phoenix-start -->\n",
        rules["phoenix.md"],
        "\n<!-- phoenix:phoenix-end -->"
      ],
      binding[:ecto] &&
        [
          "<!-- phoenix:ecto-start -->\n",
          rules["ecto.md"],
          "\n<!-- phoenix:ecto-end -->"
        ],
      binding[:html] &&
        [
          "<!-- phoenix:html-start -->\n",
          rules["html.md"],
          "\n<!-- phoenix:html-end -->"
        ],
      binding[:live] &&
        [
          "<!-- phoenix:liveview-start -->\n",
          rules["liveview.md"],
          "\n<!-- phoenix:liveview-end -->"
        ],
      "<!-- usage-rules-end -->\n"
    ]
    |> Enum.reject(fn part -> part == nil or part == false end)
    |> Enum.intersperse("\n\n")
    |> IO.iodata_to_binary()
  end

  defp load_rules_now do
    Map.new(File.ls!("usage-rules"), fn file ->
      {file, File.read!(Path.join("usage-rules", file)) |> String.trim_trailing()}
    end)
  end

  defp load_new_rules_now do
    dir = "installer/templates/usage-rules"
    Map.new(File.ls!(dir), fn file ->
      {file, File.read!(Path.join(dir, file)) |> String.trim_trailing()}
    end)
  end

  defp load_rules_base(base_ref) do
    Map.new(["ecto.md", "elixir.md", "html.md", "liveview.md", "phoenix.md"], fn file ->
      case System.cmd("git", ["show", "#{base_ref}:usage-rules/#{file}"]) do
        {content, 0} -> {file, String.trim_trailing(content)}
        _ -> {file, ""}
      end
    end)
  end

  defp load_new_rules_base(base_ref) do
    dir = "installer/templates/usage-rules"
    Map.new(["assets.md", "phoenix.md", "project.md"], fn file ->
      case System.cmd("git", ["show", "#{base_ref}:#{dir}/#{file}"]) do
        {content, 0} -> {file, String.trim_trailing(content)}
        _ -> {file, ""}
      end
    end)
  end

  defp anchor_id(file) do
    file
    |> String.downcase()
    |> String.replace(~r/[^\w\-]/, "-")
  end

  defp lang_for(file) do
    cond do
      String.ends_with?(file, ".js.eex") -> "javascript"
      String.ends_with?(file, ".pot.eex") -> "po"
      String.ends_with?(file, ".gitignore.eex") -> "gitignore"
      String.ends_with?(file, ".css") -> "css"
      String.ends_with?(file, ".exs.eex") -> "elixir"
      String.ends_with?(file, ".ex.eex") -> "elixir"
      String.contains?(file, "AGENTS.md") -> "markdown"
      true -> "text"
    end
  end

  defp get_base_ref do
    case System.cmd("git", ["merge-base", "upstream/main", "HEAD"]) do
      {hash, 0} -> String.trim(hash)
      _ ->
        case System.cmd("git", ["merge-base", "main", "HEAD"]) do
          {hash, 0} -> String.trim(hash)
          _ -> "HEAD~1"
        end
    end
  end
end

GenerateTemplateChangesReview.run()

@rhcarvalho
rhcarvalho marked this pull request as ready for review August 3, 2026 14:22
@SteffenDE
SteffenDE merged commit 51fac60 into phoenixframework:main Aug 3, 2026
8 checks passed
@SteffenDE

Copy link
Copy Markdown
Member

🙌🏻

@rhcarvalho
rhcarvalho deleted the fix-template-whitespace branch August 3, 2026 16:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants